home *** CD-ROM | disk | FTP | other *** search
/ Tech Arsenal 1 / Tech Arsenal (Arsenal Computer).ISO / tek-17 / dosdev.zip / DOSDEV.DOC < prev    next >
Text File  |  1993-01-04  |  22KB  |  210 lines

  1. _Creating User-Installable Device Drivers in MS-DOS 2.0+_
  2.  
  3.       Bruce Bordner, 1985
  4.       1713 4th Avenue
  5.       Asbury Park, NJ 07712
  6.  
  7.  
  8.  
  9.   Prior to version 2.0, driver programs to support new (or non-IBM) peripherals required some complex and ugly programming to interface with DOS.  This became so much of a problem that it was fixed in the first major revision (2.0) by providing a "legal" way to install device drivers and interface with the DOS I/O functions.  This was primarily intended as a convenience for OEMs, but Microsoft did include a chapter (14) about the subject in the DOS manual.  I haven't found any better source on the subject; which means that I found practically no other information.  [Only other source: "Modifying MS-DOS Device Drivers" by Mike Higgins, Computer Language 3/85 - a good article which can clarify the DOS documentation, but does not treat several points explained here.]
  10.   Assuming for the moment that driver software can be home-brewed, it is not an obviously useful technique.  You could make your own driver for a 500 megabyte drive rather than waiting for the manufacturer to do it, but this is a last-ditch move.  However, a driver does not necessarily have to be controlling a physical device.
  11.   A DOS device driver must be a COM file, which limits the code space to 64K total.  It has a specified header and function call structure.  DOS will only make and accept I/O operations which are given in the manual.  You cannot return an error code other than those used by DOS, and most of those are not returned to your calling program.  Other than that, you can do as you like.  This opens up many possibilities.
  12.   Driver software becomes a part of the memory-resident sections of DOS during the boot-up operation.  It becomes another resource available to any of your programs.  The classic example, as given in the DOS manual, is a ram-based disk simulator.  This is a "virtual" device, where the code and data is seen as a package by the operating system and user programs.  This module can perform any function which you can fit into a COM program, with predefined interfaces to DOS, other programs, and other device drivers.  Although Microsoft built some limiting assumptions into DOS which make it difficult to implement certain functions, many possibilities remain.  One option which I would like to explore is to offload a driver to an outboard processor for concurrent background operation.
  13.  
  14. _DOS Function Calls for Device Drivers_
  15.  
  16.   In order to keep things simple, I only used the "extended file management" (version 2.0) functions under system interrupt 21H.  These are used by the "fread()" and "fwrite()" functions of the C86 C compiler.  The primary functions affecting drivers are:
  17.  
  18.     3D  Open a file or device, return a 16-bit file handle in register
  19.     AX.
  20.  
  21.     3E  Close the file or device associated with the handle in BX.
  22.  
  23.     3F  Read from a file or device.  The following registers must be
  24.     loaded as indicated:
  25.       BX => file handle of device
  26.       CX => number of bytes to read ( 64K max)
  27.       DX => segment offset of your data buffer storage
  28.       DS => segment of your data buffer
  29.     Reads CX bytes from a device into the buffer address given.
  30.     *** What is not explained by Microsoft is that DOS actually
  31.     requests only one byte per call to the device driver, making
  32.     CX number of calls.  This has all sorts of ugly side effects,
  33.     which will become evident in the description of my sample
  34.     driver.  Microsoft has apparently built in the assumption that
  35.     all character-oriented devices talk to fairly slow serial
  36.     hardware, like printers.  If I read this right, DOS has the
  37.     ability to respond to interrupts between each byte transferred,
  38.     but it does make things clumsy.  DOS apparently increments the
  39.     DS:DX buffer location after each call.  The driver will be
  40.     re-run from START for each byte, and must maintain its own
  41.     data pointers to maintain synchronism with the DOS transfer.
  42.  
  43.     40  Write to a file or device.
  44.     Same as above, but data is transferred to your buffer area.
  45.     *** Same warning.
  46.  
  47.     44  I/O Control for Devices.  This function has 8 subfunction codes
  48.     which are used for some rudimentary device controls.
  49.     Subfunction 2 is to read CX number of bytes from the "control
  50.     channel" of the device, with the same register settings as for
  51.     the normal read.  The stated purpose is to provide a way of
  52.     reading device driver status rather than data from the device
  53.     itself.  However, up to 64K bytes made be transferred per call.
  54.     What your device does with it is up to you.
  55.     Subfunction 3 is the corresponding write call.
  56.     *** For these calls, DOS actually requests CX bytes from the
  57.     device on one call.  This is used in the second version of my
  58.     sample driver, which is much simpler than the standard
  59.     read-write calls used in the first version.
  60.  
  61. _How DOS Translates Your Read/Write Function Calls into Device Driver Requests_
  62.  
  63.   When any of the above functions is called by your application program, DOS develops a data structure called the "Request Header" by the manual.  This structure consists of a 13-byte defined header which may be followed by other data bytes depending on the function requested.  The fixed part of the request header is as follows:
  64.  
  65.   _BYTE_  _PURPOSE_
  66.     0 Length in bytes of the total request header (0-255)
  67.     1 Unit code, used to determine subunit to use in block devices
  68.       (not used for character devices)
  69.     2 Command code (0-12) to activate specific device function
  70.    3-4  Status word, returned by the driver
  71.    5-12 The manual states that this area is "reserved for DOS".
  72.     Another source indicates that this consists of two double-word
  73.     (4-byte) pointers to be used to maintain a linked list of
  74.     request headers for this device and a list of all current
  75.     device requests being processed by DOS.  This is apparently
  76.     in the works for a future concurrent-DOS.
  77.  
  78.   The 13 command codes are detailed on pages 14-12 of the manual; only the following are used by the character devices explained in this paper:
  79.  
  80.   _CODE_  _FUNCTION_
  81.     0 INIT - perform all initialization required at DOS boot time
  82.       to install the driver and set local driver variables.
  83.     3 IOCTL INPUT - read a specified number of bytes from the
  84.       device driver's IO control channel.
  85.     4 INPUT - normal device "read".  Reads a number of bytes from
  86.       the device your driver is controlling.
  87.     8 OUTPUT - normal device "write" call from user program.
  88.    12 IOCTL OUTPUT - write bytes to driver control channel.
  89.  
  90.   For each of these function calls, the driver receives the following:
  91.  
  92. INIT: This function must be built into any driver program.  It is called only by DOS during boot time, to reserve the system memory needed to hold the driver and to link the driver into the set of active devices managed by DOS.
  93. DOS sends:  13-byte request header
  94.     BYTE number of units (not used by char devices)
  95.     DWORD ending address of driver
  96.     DWORD pointer to BPB array (not used by char devices)
  97. The driver program must load the ending address at a minimum; any local initialization may also be performed at this time.
  98.  
  99. INPUT, OUTPUT, IOCTL INPUT, or IOCTL OUTPUT:
  100.   For all of these, DOS sends:
  101.   13-byte request header
  102.   BYTE media descriptor (not used for char devices)
  103.   DWORD offset and segment of the data buffer in calling program
  104.   WORD number of bytes to transfer in this call
  105.   WORD starting sector (not used for char devices)
  106. The driver must perform the requested read or write function, set the "number of bytes to transfer" location to the number actually done, and set the status word in the request header to indicate any errors.
  107.  
  108.   The actual use of these structures will be detailed in the driver function description.
  109.  
  110. _Required Structure for a Device Driver_
  111.  
  112.   Listing 1 (DOSDEV.ASM) is a template containing the minimum requirements for a character-oriented device driver.  This is detailed in pages 14-3 to 14-8 of the DOS manual.
  113.   The driver program must meet the requirements of a normal COM file.  However, COM files usually start with an ORG 100H to allow room for the DOS Program Segment Prefix structure.  For a driver, you must use ORG 0, as the PSP is not used.
  114.   The Device Header data structure must be the first object defined in your file.  It consists of:
  115.   DWORD Pointer to the next device driver currently installed.  This
  116.       should be initialized to -1, DOS will fill this field as
  117.       necessary during system initialization (boot).
  118.   WORD  Device attribute.  I used C000H to indicate that this is a
  119.       character device with IOCTL capability.  This field is also
  120.       used to indicate if this device is to be the standard output
  121.       or input device.
  122.   WORD  Pointer to "device strategy" function in the driver.  This
  123.       function is called whenever a request is made to the driver,
  124.       and must store the location of the request header from DOS.
  125.   WORD  Pointer to function which activates driver routines to perform
  126.       the command in the current request header.  This is called by
  127.       DOS after the call to the strategy function, and should reset
  128.       to the request header address stored by "strategy", to allow
  129.       for the possibility of interrupts between the two calls.
  130.   8-BYTES Name field.  For character devices, fill this with the name
  131.       which you must use when opening the device.
  132.  
  133.   After this structure, you may include any local data definitions needed for the internal operation of your driver.  The DOSDEV example includes only the minimum; a pointer to the request header and a table of addresses of the functions which will be called by the command code from DOS.  The function addresses are arranged according to their calling function code (0 to 12) so that the function router can use the DOS command code as an offset into this table.
  134.  
  135. _Required Device Driver Functions_
  136.  
  137.   For simplicity, I will discuss these functions as given in the DOSDEV.ASM listing.
  138.  
  139.   XDV STRAT:  This function is called directly by DOS when a request has been made to use this device.  Its only purpose is to save a segment and offset pointer to the request header.  At the time DOS calls the device, the segment of the request header is in register ES and the offset is in register BX.  These values are copied into the variables RH SEG and RH OFF.
  140. The fact that Microsoft calls this a "device strategy" function leads me to believe that more complex processing will be required in this function when DOS becomes multi-user or multi-processing oriented.
  141.  
  142.   XDV FUNC:  This is called by DOS immediately after XDV STRAT.  The function pushes all machine registers to save the current data until the device has finished the requested operation.  Data segment register DS is set to the Code segment value, as all local variables exist in the code segment.  Registers ES and BX are loaded from RH SEG and RH OFF to reset them to the start of the DOS request header.  The command code from the request header (at ES:[BX+2] ) is then used as an offset into the function address table FUNTAB to initiate the driver function requested.  In DOSDEV, only the INIT function has been coded, all others drop out to EXIT after setting the status word of the request header to "done; no error".  All you need to do is fill in the function code for any driver function you intend to use.
  143.  
  144.   INIT:  When DOS is booted, it reads your CONFIG.SYS file to determine which programs to install as device drivers (DEVICE=filename.ext).  After loading the file image into memory, DOS sends a request header with the command code "0" to the device.  The INIT function must load an offset (at ES:[BX+14]) and segment value (at ES:[BX+16]) into the request header to indicate the ending address for the driver program, including space for any memory used as a virtual device.  The function may also do any initial variable setting within the driver.  INIT then exits back to DOS, which uses the address given to set the boundary of DOS including the new driver storage.
  145.  
  146.   EXIT:  This function restores all machine registers and returns to DOS.
  147.  
  148. _Examples of Character-Oriented Device Drivers_
  149.  
  150.   Listing 1 (STKDEV.ASM) and 2 (STKDEV2.ASM) show the use of a virtual device driver to implement a "stack".  User programs may "push" bytes or entire records by writing them to the device, and "pop" them with a read request.  I/O control calls are used to set the record size to be used by the driver.
  151.   This may not be very useful in itself, but this example shows solutions to most of the problems without being difficult to read.
  152.   STKDEV is constructed in the recommended fashion; I got much of the code from the example device driver in the DOS manual.  Read and write calls from the user program activate the functions INPUT and OUTPUT, while IOCTL IN and IOCTL OUT are used to read and write the record size setting.  I developed the first version in a few days, but then spent two months of spare time trying to find out why it wouldn't work.  It's an undocumented feature of MS-DOS, although I can see some hints of it in the manual - now that I know what to look for.
  153.   When your user program makes a read or write call to a device, you send DOS the number of bytes to transfer, which may be 1 to 64K.  You make one call to DOS (interrupt 21H).  The request header for I/O contains a full word to contain the byte count sent from DOS.  I made the mistake of assuming that when I make a 10 byte I/O request to my driver, the driver would see a 10 byte count.  Actually, it sees 10 unrelated 1-byte requests from DOS.
  154.   STKDEV's INPUT and OUTPUT functions show the effects.  I had to establish two new variables (NUM2READ and NUM2WRITE) to keep track of how many bytes had been transferred, so that the driver would know if it was done with a "record".  This is required because the "top of stack" pointer (CURRENT) is set to the next free address following the last byte written.  A "pop" operation (INPUT) requires decrementing the pointer by the record size, transferring a full record in the byte order written, then resetting the pointer back to the used record's start to allow overwriting and repeated "pops".  There must be an easier way to do this, but I think this mess shows the problems more clearly.
  155. STKDEV2 uses IOCTL functions rather than the standard I/O.  On IOCTL calls, DOS sends the full byte count in the request header.  This made things simpler in my driver code, but complicated my user programs by requiring custom read/write functions.  Take your choice.
  156.   DOS apparently starts at the buffer address which your program supplies in the I/O call, transferring one byte with a request to the specified driver, then incrementing the buffer pointer by 1, and repeating until the specified number of bytes is copied.  The device driver must track this indexing carefully in some applications, for others it may not matter.
  157.  
  158.   Mike Higgins' article included many debugging tips.  One of them is the "yell" macro at the beginning of STKDEV.  This displays one character on the screen by writing directly to the video memory.  If you use DOS function calls to display the status of your driver, DOS will overwrite the request header which your driver has started processing.  I have left "yell" invocations throughout the function code; it was this macro that finally showed me what my device was receiving from DOS.
  159.  
  160. Functional Description of STKDEV.ASM:
  161.  
  162.   The procedure and device name is XSTK, which must be used when opening the device for I/O.  It is assembled and linked normally, then use EXE2BIN to convert the EXE file to COM form.  I used EXE2BIN STKDEV.EXE XSTK.SYS, changing the file name because any references to XSTK once it is installed cause weirdness.  The CONFIG.SYS file must contain DEVICE=XSTK.SYS.  Reboot and XSTK has added 32K+ to memory-resident DOS.
  163.  
  164. XSTK STRAT:
  165.   This is the "device strategy" function, which is called first by DOS for every request header.  DOS has set ES and BX to the address of the request header; these are stored RH SEG and RH OFF to ensure that the driver will be able to find the request header.  It may be omitted for DOS 2.0.
  166.  
  167. XSTK FUNC:
  168.   DOS calls this entry point second on all device requests.  The call is a signal to begin processing the data in the request header.  DOS is now suspended (in this version) until your device returns to it.
  169. All machine registers are saved on the stack, and ES and BX are reloaded to the address stored by XSTK STRAT.  The command code at ES:[BX+2] is used as an index to jump to the requested function.
  170.  
  171. INIT:
  172.   This function is called only by DOS, only during installation (boot) time.  As it will not be needed while the device is operating, INIT could be located after the end address returned to DOS, saving some memory.
  173.   STORAGE is the variable marking the end of the XSTK code.  However, I add 32K for stack storage.  This value is then copied to the request header and returned to DOS for memory allocation.
  174.   Local variables are set to default "stack empty" values.
  175.  
  176. IOCTL IN:
  177.   Used to read the current record size from the driver into the calling program's data buffer.
  178.   In order to use the REP MOVSB instruction, CX is set to the requested byte count, DS and SI point to the internal variable RECSIZE, ES and DI point to the buffer address contained in the request header.  SI and DI are incremented by the REPeat prefix until CX bytes have been transferred.
  179.   ES and BX are then reset to the request header address.
  180.  
  181. IOCTL OUT:
  182.   Write a new record size to the device.  Same deal as above, backward.
  183.  
  184. INPUT:
  185.   Processes read requests.  The double word at ES:[BX+14] contains the address of the data buffer in the calling program, and the word at ES:[BX+18] is the byte count for the request.  This value will always be 1 for DOS 2.0, but this is subject to change.
  186.   The first process required is to check whether the previous write (OUTPUT) completed storing RECSIZE bytes.  This is done by checking the NUM2WRITE variable.  If NUM2WRITE is not 0, the CURRENT pointer is set to a record boundary before reading.
  187.   Next, INPUT checks to see if it is in the process of reading a record or if it is starting a new record.  If NUM2READ is 0, INPUT must reset CURRENT to the start of the last record written.  At this time, INPUT checks CURRENT against BOTMEM to ensure that reads will not go past the bottom of the stack space.  I tried to return an error code of 30H, to give my calling program a different error than those used by DOS.  However, DOS apparently checks this value against the approved list, and I get a "Disk drive error" on the display.
  188. So, it appears that only the given error codes will be sent to calling programs.
  189.   The actual read transfer is set up at PULLIT.  Again, I used the REP MOVSB instruction, even though DOS will only call for one byte per request header.  CX is loaded with the count from the request header, DS and SI have been set to the proper address in the stack storage, ES and DI are set to the data buffer address of the calling program.  NUM2READ is decremented on each request.  While NUM2READ is not 0, the value of SI is stored in CURRENT; SI has been incremented by the REP MOVSB to point at the next byte of the record.  If NUM2READ is 0, a full record has been read and CURRENT must be reset to the starting address of the record.
  190.   Finally, ES and BX are reset to point to the DOS request header.  The status word of the request header is filled with the code for "done; no error" and the process completes through EXIT.
  191.  
  192. OUTPUT:
  193.   Similar to INPUT, except that CURRENT always increments.
  194.  
  195.  
  196. Description of STKDEV2:
  197.  
  198.   This version reverses the use of IOCTL and INPUT/OUTPUT.  Most of the code is the same as STKDEV, but the variables NUM2READ and NUM2WRITE are no longer needed, as the DOS request header will request the actual number of bytes given by the calling program.  THerefore, the driver implicitly knows that each request will consist of a complete record.  If you compare IOCTL IN with INPUT, and IOCTL OUT with OUTPUT of STKDEV, it is obvious that this approach was easier to code for this application.
  199.  
  200.  
  201. _Testing the Sample Device Drivers_
  202.  
  203.   Listing 4 (TXSTK.C) is a C program to perform simple test calls to STKDEV.  Listing 5 (TXSTK2.C) tests STKDEV2 by using IOCTL calls in place of the read/write system calls used in TXSTK.
  204.  
  205.   TXSTK uses segread() to determine the DS segment value of itself.  This is used to pass DOS the segment value of the data buffer "instr".  Then XSTK is opened.  I used sysint21() calls instead of fopen, fwrite() and fread() just to simplify matters.
  206.   "Outstr" is then written to XSTK.  Although I fill callregs.cx with the count of 5, I know that DOS will make 5 one-byte calls.  XSTK is currently set to the default RECSIZE of one, so a write and corresponding read produces "olleH" from my string "Hello" written.
  207.   I then use IOCTL calls to reset XSTK's RECSIZE to 5 bytes.  Although the following write and read are in the same form as before, XSTK now knows to treat input as 5-byte records.  So, "Hello" returns "Hello".
  208.  
  209.   TXSTK2 is the same through opening XSTK.  However, the first byte-at-a-time write/read must use a loop to cycle through the 5 characters of the output and input strings.  After resetting XSTK's RECSIZE to 5 using I/O calls, the write/read calls request 5 bytes, which is now processed in one call to XSTK.  The strings are returned as with TXSTK; "olleH" and "Hello".
  210.